#https://datatables.net/reference/option/
options(DT.options = list(scrollX = TRUE, pagin=TRUE, fixedHeader = TRUE, searchHighlight = TRUE))

Intro

Check out this Kaggle webpage

In one piped statement:

  1. read in data
  2. convert char to factor vars
  3. rename all colnames lowercase
  4. order cols by name: alphabetically
  5. order cols by datatype: nominal, then numeric

Get Data

a = read_csv(here::here('/Clustering/Mall_Customers.csv')) %>%  #1
  mutate(across(where(is.character),as.factor)) %>% #2
  clean_names(.) %>% #3
  select(sort(tidyselect::peek_vars())) %>% #4
  select(where(is.factor), where(is.numeric)) %>%  #5
  select(-customer_id)

#Split Data

set.seed(321)
split = a %>% initial_split()
train = split %>% training()
test = split %>% testing()

EDA: nom vars

check head rows

train %>% select(where(is.factor)) %>% head %>% DT::datatable()

glimpse structure

train %>% select(where(is.factor)) %>% glimpse
Rows: 150
Columns: 1
$ gender <fct> Male, Male, Female, Female, Female, Female, Female, Male, Female, Male, Female, Female...

check for missing values

train %>% select(where(is.factor)) %>% miss_var_summary()

distribution: counts of unique levels

sapply(train %>% select(where(is.factor)), n_unique)
gender 
     2 

reference: names of unique levels

sapply(train %>% select(where(is.factor)), unique)
     gender  
[1,] "Male"  
[2,] "Female"

binarize gender to numeric var

train = train %>% mutate(gender = if_else(gender == 'Male', 1, 0))

distribution: viz

ggplotly(
train %>% count(gender = factor(gender)) %>%
  mutate(percent = n/nrow(train)) %>%
  ggplot(aes(percent, gender, fill = gender)) +
  geom_col() +
  scale_x_continuous(labels = scales::percent) +
  labs(x = '', y = '', title ='Gender Percent Breakdown: 1 = Male, 0 = Female') +
  theme(legend.position = 'none')
)

EDA: num vars

check head rows

train %>% select(where(is.numeric)) %>% head %>% DT::datatable()

glimpse structure

train %>% select(where(is.numeric)) %>% glimpse
Rows: 150
Columns: 4
$ gender               <dbl> 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, ...
$ age                  <dbl> 19, 21, 20, 31, 22, 35, 23, 64, 30, 67, 35, 24, 37, 22, 52, 35, 35, 25, ...
$ annual_income_k      <dbl> 15, 15, 16, 17, 17, 18, 18, 19, 19, 19, 19, 20, 20, 20, 23, 23, 24, 24, ...
$ spending_score_1_100 <dbl> 39, 81, 6, 40, 76, 6, 94, 3, 72, 14, 99, 77, 13, 79, 29, 98, 35, 73, 73,...

check for missing values

miss_var_summary(train %>% select(where(is.numeric)))
NA

distribution: viz

There appears to be outliers for male annual income

distribution: viz

distribution: viz

pairwise correlations: viz

GGally::ggcorr(train %>% select(where(is.numeric)), low = '#990000', mid = '#E0E0E0', high = '#009900', label = TRUE)

Preprocessing

Reference: (package recipes)[https://recipes.tidymodels.org/reference/index.html]

normalize data

Create matrix

Determine Optimal Number of Clusters

Documentation

1) silhouette analysis with kmeans and euclidean distancing

factoextra::fviz_nbclust(
  train.matrix,
  diss = dist(train.matrix, method = "euclidean"),
  FUNcluster=kmeans,
  method="silhouette"
  ) +
  theme_classic()

2) silhouette analysis with kmeans and manhattan distancing

factoextra::fviz_nbclust(
  train.matrix,
  diss = dist(train.matrix, method = "manhattan"),
  FUNcluster=cluster::pam,
  method="silhouette"
  ) +
  theme_classic()

3) silhouette analysis with pam and euclidean distancing

factoextra::fviz_nbclust(
  train.matrix,
  diss = dist(train.matrix, method = "euclidean"),
  FUNcluster=kmeans,
  method="silhouette"
  ) +
  theme_classic()

4) silhouette analysis with pam and manhattan distancing

factoextra::fviz_nbclust(
  train.matrix,
  diss = dist(train.matrix, method = "manhattan"),
  FUNcluster=cluster::pam,
  method="silhouette"
  ) +
  theme_classic()

Finalize Models with Optimal Number of Clusters and Distancing Method

Kmeans

km = eclust(
  train.matrix,
  FUNcluster="kmeans",
  k=9,
  hc_metric = "manhattan"
  )

PAM

pam = eclust(
  train.matrix,
  FUNcluster="pam",
  k=9,
  hc_metric = "manhattan"
  )

train %>%
  mutate(gender = factor(if_else(gender == 1, 'Male', 'Female'))) %>%
  group_by(cluster, gender) %>%
  summarise(
    mean.age = mean(age, na.rm = TRUE),
    mean.income = mean(annual_income_k, na.rm = TRUE),
    mean.spending.score = mean(spending_score_1_100, na.rm = TRUE)
  )
`summarise()` regrouping output by 'cluster' (override with `.groups` argument)
xvars = train %>% select(-cluster) %>% names %>% as.character()

jpal = colorRampPalette(RColorBrewer::brewer.pal(8,'Dark2'))(25)

train %>% plot_ly(y = ~cluster, x = ~eval(as.name(xvars[2])), color = ~cluster, colors = jpal) %>% add_boxplot() %>% hide_legend() %>% layout(
  title = paste0(xvars[2],' by cluster'), xaxis = list(title = xvars[2]))


train %>% plot_ly(y = ~cluster, x = ~eval(as.name(xvars[3])), color = ~cluster, colors = jpal) %>% add_boxplot() %>% hide_legend() %>% layout(
  title = paste0(xvars[3],' by cluster'), xaxis = list(title = xvars[3]))


train %>% plot_ly(y = ~cluster, x = ~eval(as.name(xvars[4])), color = ~cluster, colors = jpal) %>% add_boxplot() %>% hide_legend() %>% layout(
  title = paste0(xvars[4],' by cluster'), xaxis = list(title = xvars[4]))

Notes

  1. Try Other Popular Clustering Methods
    • hierarchical clustering
    • dbscan
LS0tDQp0aXRsZTogIkNsdXN0ZXJpbmcgTWFsbCBDdXN0b21lcnMiDQphdXRob3I6ICJKZXJlbWlhaCBXIg0KZGF0ZTogJ09jdG9iZXIgMjggMjAyMCcNCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDoNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogJzInDQogICAgZGZfcHJpbnQ6IHBhZ2VkDQogIGh0bWxfbm90ZWJvb2s6DQogICAgdGhlbWU6IHBhcGVyDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgZGZfcHJpbnQ6IGhpZGUNCiAgICB0b2M6IHllcw0KICAgIHRvY19kZXB0aDogMg0KICAgIHRvY19mbG9hdDoNCiAgICAgIGNvbGxwYXNlZDogbm8NCiAgICAgIHNtb290aF9zY3JvbGw6IG5vDQotLS0NCg0KYGBge3J9DQojaHR0cHM6Ly9kYXRhdGFibGVzLm5ldC9yZWZlcmVuY2Uvb3B0aW9uLw0Kb3B0aW9ucyhEVC5vcHRpb25zID0gbGlzdChzY3JvbGxYID0gVFJVRSwgcGFnaW49VFJVRSwgZml4ZWRIZWFkZXIgPSBUUlVFLCBzZWFyY2hIaWdobGlnaHQgPSBUUlVFKSkNCmBgYA0KDQpgYGB7ciBpbmNsdWRlPUZBTFNFfQ0KbGlicmFyeShEYXRhRXhwbG9yZXIpO2xpYnJhcnkoZGF0YS50YWJsZSk7DQpsaWJyYXJ5KGV4dHJhZm9udCk7bGlicmFyeShmb3JtYXR0YWJsZSk7bGlicmFyeShHR2FsbHkpO2xpYnJhcnkoaGVyZSk7DQpsaWJyYXJ5KGphbml0b3IpO2xpYnJhcnkobHVicmlkYXRlKTtsaWJyYXJ5KG5hbmlhcik7DQpsaWJyYXJ5KHBhdGNod29yayk7bGlicmFyeShQZXJmb3JtYW5jZUFuYWx5dGljcyk7DQpsaWJyYXJ5KHBsb3RseSk7bGlicmFyeShSQ29sb3JCcmV3ZXIpO2xpYnJhcnkocmVhZHhsKTsNCmxpYnJhcnkoc2tpbXIpO2xpYnJhcnkodGlkeXZlcnNlKTtsaWJyYXJ5KHNjYWxlcykNCg0KbGlicmFyeShjYXJldCk7bGlicmFyeSh0aWR5bW9kZWxzKTtsaWJyYXJ5KGgybykNCg0KbGlicmFyeShrbWVkKTtsaWJyYXJ5KE5iQ2x1c3QpO2xpYnJhcnkoZmFjdG9leHRyYSkNCmBgYA0KDQojIEludHJvDQpDaGVjayBvdXQgW3RoaXMgS2FnZ2xlIHdlYnBhZ2VdKGh0dHBzOi8vd3d3LmthZ2dsZS5jb20vdmpjaG91ZGhhcnk3L2N1c3RvbWVyLXNlZ21lbnRhdGlvbi10dXRvcmlhbC1pbi1weXRob24pDQoNCkluIG9uZSBwaXBlZCBzdGF0ZW1lbnQ6DQogIA0KMS4gcmVhZCBpbiBkYXRhDQoyLiBjb252ZXJ0IGNoYXIgdG8gZmFjdG9yIHZhcnMNCjMuIHJlbmFtZSBhbGwgY29sbmFtZXMgbG93ZXJjYXNlDQo0LiBvcmRlciBjb2xzIGJ5IG5hbWU6IGFscGhhYmV0aWNhbGx5DQo1LiBvcmRlciBjb2xzIGJ5IGRhdGF0eXBlOiBub21pbmFsLCB0aGVuIG51bWVyaWMNCg0KIyBHZXQgRGF0YQ0KYGBge3IgbWVzc2FnZT1GQUxTRX0NCmEgPSByZWFkX2NzdihoZXJlOjpoZXJlKCcvQ2x1c3RlcmluZy9NYWxsX0N1c3RvbWVycy5jc3YnKSkgJT4lICAjMQ0KICBtdXRhdGUoYWNyb3NzKHdoZXJlKGlzLmNoYXJhY3RlciksYXMuZmFjdG9yKSkgJT4lICMyDQogIGNsZWFuX25hbWVzKC4pICU+JSAjMw0KICBzZWxlY3Qoc29ydCh0aWR5c2VsZWN0OjpwZWVrX3ZhcnMoKSkpICU+JSAjNA0KICBzZWxlY3Qod2hlcmUoaXMuZmFjdG9yKSwgd2hlcmUoaXMubnVtZXJpYykpICU+JSAgIzUNCiAgc2VsZWN0KC1jdXN0b21lcl9pZCkNCmBgYA0KDQojU3BsaXQgRGF0YQ0KYGBge3J9DQpzZXQuc2VlZCgzMjEpDQpzcGxpdCA9IGEgJT4lIGluaXRpYWxfc3BsaXQoKQ0KdHJhaW4gPSBzcGxpdCAlPiUgdHJhaW5pbmcoKQ0KdGVzdCA9IHNwbGl0ICU+JSB0ZXN0aW5nKCkNCmBgYA0KDQojIEVEQTogbm9tIHZhcnMNCg0KIyMjIGNoZWNrIGhlYWQgcm93cw0KYGBge3J9DQp0cmFpbiAlPiUgc2VsZWN0KHdoZXJlKGlzLmZhY3RvcikpICU+JSBoZWFkICU+JSBEVDo6ZGF0YXRhYmxlKCkNCmBgYA0KIyMjIGdsaW1wc2Ugc3RydWN0dXJlDQpgYGB7cn0NCnRyYWluICU+JSBzZWxlY3Qod2hlcmUoaXMuZmFjdG9yKSkgJT4lIGdsaW1wc2UNCmBgYA0KDQojIyMgY2hlY2sgZm9yIG1pc3NpbmcgdmFsdWVzDQpgYGB7cn0NCnRyYWluICU+JSBzZWxlY3Qod2hlcmUoaXMuZmFjdG9yKSkgJT4lIG1pc3NfdmFyX3N1bW1hcnkoKQ0KYGBgDQojIyMgZGlzdHJpYnV0aW9uOiBjb3VudHMgb2YgdW5pcXVlIGxldmVscw0KYGBge3J9DQpzYXBwbHkodHJhaW4gJT4lIHNlbGVjdCh3aGVyZShpcy5mYWN0b3IpKSwgbl91bmlxdWUpDQpgYGANCg0KIyMjIHJlZmVyZW5jZTogbmFtZXMgb2YgdW5pcXVlIGxldmVscw0KYGBge3J9DQpzYXBwbHkodHJhaW4gJT4lIHNlbGVjdCh3aGVyZShpcy5mYWN0b3IpKSwgdW5pcXVlKQ0KYGBgDQojIyMgYmluYXJpemUgZ2VuZGVyIHRvIG51bWVyaWMgdmFyDQpgYGB7cn0NCnRyYWluID0gdHJhaW4gJT4lIG11dGF0ZShnZW5kZXIgPSBpZl9lbHNlKGdlbmRlciA9PSAnTWFsZScsIDEsIDApKQ0KYGBgDQoNCg0KIyMjIGRpc3RyaWJ1dGlvbjogdml6DQpgYGB7ciBjYWNoZT1UUlVFfQ0KZ2dwbG90bHkoDQp0cmFpbiAlPiUgY291bnQoZ2VuZGVyID0gZmFjdG9yKGdlbmRlcikpICU+JQ0KICBtdXRhdGUocGVyY2VudCA9IG4vbnJvdyh0cmFpbikpICU+JQ0KICBnZ3Bsb3QoYWVzKHBlcmNlbnQsIGdlbmRlciwgZmlsbCA9IGdlbmRlcikpICsNCiAgZ2VvbV9jb2woKSArDQogIHNjYWxlX3hfY29udGludW91cyhsYWJlbHMgPSBzY2FsZXM6OnBlcmNlbnQpICsNCiAgbGFicyh4ID0gJycsIHkgPSAnJywgdGl0bGUgPSdHZW5kZXIgUGVyY2VudCBCcmVha2Rvd246IDEgPSBNYWxlLCAwID0gRmVtYWxlJykgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb24gPSAnbm9uZScpDQopDQpgYGANCg0KIyBFREE6IG51bSB2YXJzDQoNCiMjIyBjaGVjayBoZWFkIHJvd3MNCmBgYHtyfQ0KdHJhaW4gJT4lIHNlbGVjdCh3aGVyZShpcy5udW1lcmljKSkgJT4lIGhlYWQgJT4lIERUOjpkYXRhdGFibGUoKQ0KYGBgDQoNCiMjIyBnbGltcHNlIHN0cnVjdHVyZQ0KYGBge3J9DQp0cmFpbiAlPiUgc2VsZWN0KHdoZXJlKGlzLm51bWVyaWMpKSAlPiUgZ2xpbXBzZQ0KYGBgDQoNCiMjIyBjaGVjayBmb3IgbWlzc2luZyB2YWx1ZXMNCmBgYHtyICByb3dzLnByaW50ID0gMTB9DQptaXNzX3Zhcl9zdW1tYXJ5KHRyYWluICU+JSBzZWxlY3Qod2hlcmUoaXMubnVtZXJpYykpKQ0KDQpgYGANCg0KIyMjIGRpc3RyaWJ1dGlvbjogdml6DQpgYGB7ciBjYWNoZT1UUlVFfQ0KRGF0YUV4cGxvcmVyOjpwbG90X2JveHBsb3QodHJhaW4gJT4lIHNlbGVjdCh3aGVyZShpcy5udW1lcmljKSwgZ2VuZGVyKSwgYnkgPSAnZ2VuZGVyJykNCmBgYA0KDQo8aDMgc3R5bGU9ImNvbG9yOiByZWQ7IGZvbnQtc2l6ZToxNHB4OyI+VGhlcmUgYXBwZWFycyB0byBiZSBvdXRsaWVycyBmb3IgbWFsZSBhbm51YWwgaW5jb21lPC9oMj4NCg0KIyMjIGRpc3RyaWJ1dGlvbjogdml6DQpgYGB7ciBjYWNoZT1UUlVFfQ0KRGF0YUV4cGxvcmVyOjpwbG90X2hpc3RvZ3JhbSh0cmFpbiAlPiUgc2VsZWN0KHdoZXJlKGlzLm51bWVyaWMpKSkNCmBgYA0KDQojIyMgZGlzdHJpYnV0aW9uOiB2aXoNCmBgYHtyIGNhY2hlPVRSVUV9DQpEYXRhRXhwbG9yZXI6OnBsb3RfZGVuc2l0eSh0cmFpbiAlPiUgc2VsZWN0KHdoZXJlKGlzLm51bWVyaWMpKSkNCmBgYA0KDQojIyMgcGFpcndpc2UgY29ycmVsYXRpb25zOiB2aXoNCmBgYHtyIGNhY2hlPVRSVUV9DQpHR2FsbHk6OmdnY29ycih0cmFpbiAlPiUgc2VsZWN0KHdoZXJlKGlzLm51bWVyaWMpKSwgbG93ID0gJyM5OTAwMDAnLCBtaWQgPSAnI0UwRTBFMCcsIGhpZ2ggPSAnIzAwOTkwMCcsIGxhYmVsID0gVFJVRSkNCmBgYA0KDQojIFByZXByb2Nlc3NpbmcNClJlZmVyZW5jZTogKHBhY2thZ2UgcmVjaXBlcylbaHR0cHM6Ly9yZWNpcGVzLnRpZHltb2RlbHMub3JnL3JlZmVyZW5jZS9pbmRleC5odG1sXQ0KDQojIyMgbm9ybWFsaXplIGRhdGENCmBgYHtyfQ0KIyMjIG5vcm1hbGl6ZSBkYXRhIHNvIGNlcnRhaW4gZmVhdHVyZXMgYXJlbid0IHVuZmFpcmx5IHdlaWdodGVkDQpyZWNpcGUgPSB0cmFpbiAlPiUgcmVjaXBlKCkgJT4lIHN0ZXBfbm9ybWFsaXplKGFsbF9udW1lcmljKCkpDQoNCnRyYWluLm5vcm1hbGl6ZWQgPSByZWNpcGUgJT4lIHByZXAoKSAlPiUganVpY2UNCmBgYA0KIyMgQ3JlYXRlIG1hdHJpeA0KYGBge3IgY2FjaGU9VFJVRSwgZWNobz1GQUxTRX0NCnRyYWluLm1hdHJpeCA9IHRyYWluLm5vcm1hbGl6ZWQgJT4lIGFzLm1hdHJpeCgpDQpgYGANCg0KIyBEZXRlcm1pbmUgT3B0aW1hbCBOdW1iZXIgb2YgQ2x1c3RlcnMNCg0KW0RvY3VtZW50YXRpb25dKGh0dHBzOi8vcnN0dWRpby1wdWJzLXN0YXRpYy5zMy5hbWF6b25hd3MuY29tLzQ1NTM5M19mMjBiYWNmMTMyOWE0OWRhYjQwZWIzOTMzMDhiMzNlYi5odG1sI2Nob29zaW5nLW9wdGltYWwtbnVtYmVyLW9mLWNsdXN0ZXJzKQ0KDQojIyMgMSkgc2lsaG91ZXR0ZSBhbmFseXNpcyB3aXRoIGttZWFucyBhbmQgZXVjbGlkZWFuIGRpc3RhbmNpbmcNCmBgYHtyfQ0KZmFjdG9leHRyYTo6ZnZpel9uYmNsdXN0KA0KICB0cmFpbi5tYXRyaXgsDQogIGRpc3MgPSBkaXN0KHRyYWluLm1hdHJpeCwgbWV0aG9kID0gImV1Y2xpZGVhbiIpLA0KICBGVU5jbHVzdGVyPWttZWFucywNCiAgbWV0aG9kPSJzaWxob3VldHRlIg0KICApICsNCiAgdGhlbWVfY2xhc3NpYygpDQpgYGANCg0KIyMjIDIpIHNpbGhvdWV0dGUgYW5hbHlzaXMgd2l0aCBrbWVhbnMgYW5kIG1hbmhhdHRhbiBkaXN0YW5jaW5nDQpgYGB7cn0NCmZhY3RvZXh0cmE6OmZ2aXpfbmJjbHVzdCgNCiAgdHJhaW4ubWF0cml4LA0KICBkaXNzID0gZGlzdCh0cmFpbi5tYXRyaXgsIG1ldGhvZCA9ICJtYW5oYXR0YW4iKSwNCiAgRlVOY2x1c3Rlcj1jbHVzdGVyOjpwYW0sDQogIG1ldGhvZD0ic2lsaG91ZXR0ZSINCiAgKSArDQogIHRoZW1lX2NsYXNzaWMoKQ0KYGBgDQoNCiMjIyAzKSBzaWxob3VldHRlIGFuYWx5c2lzIHdpdGggcGFtIGFuZCBldWNsaWRlYW4gZGlzdGFuY2luZw0KYGBge3J9DQpmYWN0b2V4dHJhOjpmdml6X25iY2x1c3QoDQogIHRyYWluLm1hdHJpeCwNCiAgZGlzcyA9IGRpc3QodHJhaW4ubWF0cml4LCBtZXRob2QgPSAiZXVjbGlkZWFuIiksDQogIEZVTmNsdXN0ZXI9a21lYW5zLA0KICBtZXRob2Q9InNpbGhvdWV0dGUiDQogICkgKw0KICB0aGVtZV9jbGFzc2ljKCkNCmBgYA0KDQojIyMgNCkgc2lsaG91ZXR0ZSBhbmFseXNpcyB3aXRoIHBhbSBhbmQgbWFuaGF0dGFuIGRpc3RhbmNpbmcNCmBgYHtyfQ0KZmFjdG9leHRyYTo6ZnZpel9uYmNsdXN0KA0KICB0cmFpbi5tYXRyaXgsDQogIGRpc3MgPSBkaXN0KHRyYWluLm1hdHJpeCwgbWV0aG9kID0gIm1hbmhhdHRhbiIpLA0KICBGVU5jbHVzdGVyPWNsdXN0ZXI6OnBhbSwNCiAgbWV0aG9kPSJzaWxob3VldHRlIg0KICApICsNCiAgdGhlbWVfY2xhc3NpYygpDQpgYGANCg0KIyBGaW5hbGl6ZSBNb2RlbHMgd2l0aCBPcHRpbWFsIE51bWJlciBvZiBDbHVzdGVycyBhbmQgRGlzdGFuY2luZyBNZXRob2QNCg0KIyMgS21lYW5zDQpgYGB7cn0NCmttID0gZWNsdXN0KA0KICB0cmFpbi5tYXRyaXgsDQogIEZVTmNsdXN0ZXI9ImttZWFucyIsDQogIGs9OSwNCiAgaGNfbWV0cmljID0gIm1hbmhhdHRhbiINCiAgKQ0KYGBgDQoNCiMjIFBBTQ0KYGBge3J9DQpwYW0gPSBlY2x1c3QoDQogIHRyYWluLm1hdHJpeCwNCiAgRlVOY2x1c3Rlcj0icGFtIiwNCiAgaz05LA0KICBoY19tZXRyaWMgPSAibWFuaGF0dGFuIg0KICApDQpgYGANCg0KDQpgYGB7cn0NCnRyYWluID0gdHJhaW4gJT4lIG11dGF0ZShjbHVzdGVyID0gZmFjdG9yKHBhbSRjbHVzdGVyLCBsZXZlbHMgPSAxOjkpKQ0KDQp0cmFpbiAlPiUNCiAgbXV0YXRlKGdlbmRlciA9IGZhY3RvcihpZl9lbHNlKGdlbmRlciA9PSAxLCAnTWFsZScsICdGZW1hbGUnKSkpICU+JQ0KICBncm91cF9ieShjbHVzdGVyLCBnZW5kZXIpICU+JQ0KICBzdW1tYXJpc2UoDQogICAgbWVhbi5hZ2UgPSBtZWFuKGFnZSwgbmEucm0gPSBUUlVFKSwNCiAgICBtZWFuLmluY29tZSA9IG1lYW4oYW5udWFsX2luY29tZV9rLCBuYS5ybSA9IFRSVUUpLA0KICAgIG1lYW4uc3BlbmRpbmcuc2NvcmUgPSBtZWFuKHNwZW5kaW5nX3Njb3JlXzFfMTAwLCBuYS5ybSA9IFRSVUUpDQogICkNCmBgYA0KYGBge3J9DQpnbGltcHNlKHRyYWluKQ0KdHJhaW4gJT4lIGNvdW50KGNsdXN0ZXIpICU+JSBhcnJhbmdlKC1uKQ0KYGBgDQpgYGB7cn0NCmdncGxvdGx5KHRyYWluICU+JSBtdXRhdGUoZ2VuZGVyID0gZmFjdG9yKGlmX2Vsc2UoZ2VuZGVyID09IDEsICdNYWxlJywgJ0ZlbWFsZScpKSkgJT4lIGdncGxvdChhZXMoY2x1c3RlciwgYW5udWFsX2luY29tZV9rLCBmaWxsID0gY2x1c3RlcikpICsgZ2VvbV9ib3hwbG90KCkgKyBmYWNldF93cmFwKH5nZW5kZXIpICsNCiAgICAgICAgICAgc2NhbGVfeV9jb250aW51b3VzKGJyZWFrcyA9IHNlcSgNCiAgICAgICAgICAgICBtaW4odHJhaW4kYW5udWFsX2luY29tZV9rKSwNCiAgICAgICAgICAgICBtYXgodHJhaW4kYW5udWFsX2luY29tZV9rKSwgMTANCiAgICAgICAgICAgICApKSkNCg0KZ2dwbG90bHkodHJhaW4gJT4lIG11dGF0ZShnZW5kZXIgPSBmYWN0b3IoaWZfZWxzZShnZW5kZXIgPT0gMSwgJ01hbGUnLCAnRmVtYWxlJykpKSAlPiUgZ2dwbG90KGFlcyhjbHVzdGVyLCBhZ2UsIGZpbGwgPSBjbHVzdGVyKSkgKyBnZW9tX2JveHBsb3QoKSArIGZhY2V0X3dyYXAofmdlbmRlcikpDQoNCmdncGxvdGx5KHRyYWluICU+JSBtdXRhdGUoZ2VuZGVyID0gZmFjdG9yKGlmX2Vsc2UoZ2VuZGVyID09IDEsICdNYWxlJywgJ0ZlbWFsZScpKSkgJT4lIGdncGxvdChhZXMoY2x1c3Rlciwgc3BlbmRpbmdfc2NvcmVfMV8xMDAsIGZpbGwgPSBjbHVzdGVyKSkgKyBnZW9tX2JveHBsb3QoKSArIGZhY2V0X3dyYXAofmdlbmRlcikpDQoNCmBgYA0KDQpgYGB7cn0NCnh2YXJzID0gdHJhaW4gJT4lIHNlbGVjdCgtY2x1c3RlcikgJT4lIG5hbWVzICU+JSBhcy5jaGFyYWN0ZXIoKQ0KDQpqcGFsID0gY29sb3JSYW1wUGFsZXR0ZShSQ29sb3JCcmV3ZXI6OmJyZXdlci5wYWwoOCwnRGFyazInKSkoMjUpDQoNCnRyYWluICU+JSBwbG90X2x5KHkgPSB+Y2x1c3RlciwgeCA9IH5ldmFsKGFzLm5hbWUoeHZhcnNbMl0pKSwgY29sb3IgPSB+Y2x1c3RlciwgY29sb3JzID0ganBhbCkgJT4lIGFkZF9ib3hwbG90KCkgJT4lIGhpZGVfbGVnZW5kKCkgJT4lIGxheW91dCgNCiAgdGl0bGUgPSBwYXN0ZTAoeHZhcnNbMl0sJyBieSBjbHVzdGVyJyksIHhheGlzID0gbGlzdCh0aXRsZSA9IHh2YXJzWzJdKSkNCg0KdHJhaW4gJT4lIHBsb3RfbHkoeSA9IH5jbHVzdGVyLCB4ID0gfmV2YWwoYXMubmFtZSh4dmFyc1szXSkpLCBjb2xvciA9IH5jbHVzdGVyLCBjb2xvcnMgPSBqcGFsKSAlPiUgYWRkX2JveHBsb3QoKSAlPiUgaGlkZV9sZWdlbmQoKSAlPiUgbGF5b3V0KA0KICB0aXRsZSA9IHBhc3RlMCh4dmFyc1szXSwnIGJ5IGNsdXN0ZXInKSwgeGF4aXMgPSBsaXN0KHRpdGxlID0geHZhcnNbM10pKQ0KDQp0cmFpbiAlPiUgcGxvdF9seSh5ID0gfmNsdXN0ZXIsIHggPSB+ZXZhbChhcy5uYW1lKHh2YXJzWzRdKSksIGNvbG9yID0gfmNsdXN0ZXIsIGNvbG9ycyA9IGpwYWwpICU+JSBhZGRfYm94cGxvdCgpICU+JSBoaWRlX2xlZ2VuZCgpICU+JSBsYXlvdXQoDQogIHRpdGxlID0gcGFzdGUwKHh2YXJzWzRdLCcgYnkgY2x1c3RlcicpLCB4YXhpcyA9IGxpc3QodGl0bGUgPSB4dmFyc1s0XSkpDQpgYGANCg0KYGBge3J9DQoNCmBgYA0KDQoNCiMgTm90ZXMNCg0KMS4gVHJ5IE90aGVyIFBvcHVsYXIgQ2x1c3RlcmluZyBNZXRob2RzDQogICAgKyBoaWVyYXJjaGljYWwgY2x1c3RlcmluZw0KICAgICsgZGJzY2Fu